﻿using System.Runtime.Serialization;
using Decisions.HL7.Generated.AbstractMessage;
using Decisions.HL7.Message;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using DecisionsFramework;
using Decisions.HL7.Parser;

namespace Decisions.HL7.DataTypes
{
    [DataContract]
    public abstract class AbstractHL7ParseableEntity
    {
        public static Log LOG = new Log("HL7 Engine");

        public AbstractHL7ParseableEntity()
        {
            HadParserError = false;
        }

        protected abstract Dictionary<string, bool> MAP_OF_SEGMENT_TO_REQUIRED
        {
            get;
        }

        public abstract string HL7ENTITY_TYPE_NAME
        {
            get;
        }

        #region Parser Information Properties

        [DataMember]
        public bool HadParserError { get; set; }

        [DataMember]
        public string[] ParserMessages { 
            
            get {
                return parserMessages.ToArray();
            }
            set {
                if (value == null) {
                    parserMessages = new List<string>();
                    return;
                }
                if (parserMessages == null) {
                    parserMessages = new List<string>();
                }
                parserMessages.Clear();
                parserMessages.AddRange(value);

            }
        }

        private List<string> parserMessages = new List<string>();
        public void AddParserMessage(string message) {

            parserMessages.Add(message);

        }

        #endregion


        #region Parsing ER7 into Object Model

        protected T PopSegmentParseAndCheckRequired<T>(Stack<string> availableData, HL7ParserOptions options, string name) where T : AbstractSegment, new()
        {
            if (availableData.Count > 0)
            {
                if (availableData.Peek() != null && availableData.Peek().StartsWith(name.Substring(0, 3), StringComparison.OrdinalIgnoreCase))
                {
                    T segment = new T();
                    segment.ConsumeTextSegment(availableData.Pop(), options);
                    return segment;
                }
                else 
                {
                    if (options.ParserMode == ParserMode.Tolerant) { 
                        // We do not have the segment that matches this one, but 
                        // it might come after this segment if we are in tolerant mode
                        // we ALLOW this and just try to find our right segment!
                        string matchingSegment = availableData.FirstOrDefault(x => x.StartsWith(name.Substring(0, 3), StringComparison.OrdinalIgnoreCase));
                        if (matchingSegment != null) {
                            
                            // Fix up stack to remove this entry....
                            List<string> currentStack = availableData.ToList();
                            currentStack.Remove(matchingSegment);
                            currentStack.Reverse();
                            availableData.Clear();
                            currentStack.ForEach(x => availableData.Push(x)); // Put the remainder back.

                            T segment = new T();
                            segment.ConsumeTextSegment(matchingSegment, options);
                            return segment;
                        }
                    }

                    if (IsRequired(name, options.Version))
                    {
                        HadParserError = true;

                        string found = availableData.Peek() != null ? availableData.Peek().Substring(0, 3) : "NO MORE DATA";
                        string message = string.Format("Looking for REQUIRED {0} but found {1}.  This segment might be discarded if it is out of order.", name.Substring(0, 3), found);
                        AddParserMessage(message);
                        if (LogRegistry.Logger.IsDebugEnabled())
                        {
                            LogRegistry.Logger.LogWarn(message);
                        }

                        // If this was required, and we're strict throw an error.
                        if (options.ParserMode == Parser.ParserMode.Strict)
                        {
                            throw new SegmentRequiredException(name, HL7ENTITY_TYPE_NAME, availableData.Peek());
                        }
                    }
                    else {
                        AddParserMessage(string.Format("Could not find segment {0}.  Message might be out of order.", name));
                    }
                }
            }
            return null;
        }

        protected T[] PopSegmentParseAndCheckRequiredArray<T>(Stack<string> availableData, HL7ParserOptions options, string name) where T : AbstractSegment, new()
        {
            if (availableData.Count > 0)
            {

                List<T> resultingArray = new List<T>();
                while (availableData.Count > 0 && availableData.Peek() != null && availableData.Peek().StartsWith(name.Substring(0, 3), StringComparison.OrdinalIgnoreCase))
                {
                    T newSegment = new T();
                    newSegment.ConsumeTextSegment(availableData.Pop(), options);
                    resultingArray.Add(newSegment);
                }
                if (resultingArray.Count == 0)
                {

                    if (IsRequired(name, options.Version))
                    {
                        HadParserError = true;

                        string found = availableData.Peek() != null ? availableData.Peek().Substring(0, 3) : "NO MORE DATA";
                        string message = string.Format("Looking for REQUIRED {0} but found {1}.  This segment might be discarded if it is out of order.", name.Substring(0, 3), found);
                        AddParserMessage(message);
                        if (LogRegistry.Logger.IsDebugEnabled())
                        {
                            LogRegistry.Logger.LogWarn(message);
                        }


                        if (options.ParserMode == Parser.ParserMode.Strict)
                        {
                            throw new SegmentRequiredException(name, HL7ENTITY_TYPE_NAME, availableData.Peek());
                        }
                    }
                    else {
                        AddParserMessage(string.Format("Could not find segment {0}.  Message might be out of order.", name));
                    }
                }
                return resultingArray.ToArray();
            }
            // No available data, just return
            return null;
        }

        protected T1 ProcessGroup<T1>(Stack<string> availableData, HL7ParserOptions options, string segmentGroupName) where T1 : AbstractGroup, new()
        {
            if (availableData.Count > 0)
            {
                T1 newGroup = new T1();
                int segmentsBeforeGroup = availableData.Count;

                try
                {
                    newGroup.ConsumeTextSegmentsForGroup(availableData, options);

                    if (segmentsBeforeGroup == availableData.Count && IsRequired(segmentGroupName, options.Version))
                    {
                        HadParserError = true;

                        if (options.ParserMode == Parser.ParserMode.Strict)
                        {
                            throw new SegmentRequiredException(segmentGroupName, HL7ENTITY_TYPE_NAME, availableData.Peek());
                        }
                    }

                    return newGroup;
                }
                catch (SegmentRequiredException) { 
                    // Required Segment found, but if this group is optional we 
                    // should skip it!
                    if (IsRequired(segmentGroupName, options.Version) == false) {
                        LOG.LogDebug(null, "Required segment error swallowed because owning group {0} not required.", segmentGroupName);
                    }
                }
            }
            return null;
        }

        protected T[] ProcessGroupArray<T>(Stack<string> availableData, HL7ParserOptions options, string segmentGroupName) where T : AbstractGroup, new()
        {
            if (availableData.Count > 0)
            {
                List<T> resultingArray = new List<T>();
                T newSegment = new T();
                while (availableData.Count > 0 && availableData.Peek() != null && newSegment.CanConsume(availableData.Peek(), options))
                {
                    newSegment.ConsumeTextSegmentsForGroup(availableData, options);
                    resultingArray.Add(newSegment);
                    newSegment = new T();
                }
                if (resultingArray.Count == 0)
                {
                    if (IsRequired(segmentGroupName, options.Version))
                    {
                        HadParserError = true;

                        if (options.ParserMode == Parser.ParserMode.Strict)
                        {
                            throw new SegmentRequiredException(segmentGroupName, HL7ENTITY_TYPE_NAME, availableData.Peek());
                        }
                    }
                }
                return resultingArray.ToArray();
            }
            // No available data to consume.
            return null;
        }

        public bool IsRequired(string segmentInQuestion, HL7Version version)
        {
            string mapKey = segmentInQuestion + version;
            if (MAP_OF_SEGMENT_TO_REQUIRED.ContainsKey(mapKey))
            {
                return MAP_OF_SEGMENT_TO_REQUIRED[mapKey];
            }
            else
            {
                return false;
            }
        }

        protected static void SetupStaticPropertyMap(Dictionary<string, bool> segmentRequirementMap, Type messageType)
        {
            foreach (PropertyInfo pi in messageType.GetProperties())
            {

                object[] fieldAttributes = pi.GetCustomAttributes(typeof(HL7FieldAttribute), true);

                foreach (HL7FieldAttribute eachFieldAttribute in fieldAttributes)
                {

                    segmentRequirementMap[pi.Name + eachFieldAttribute.Version] = (eachFieldAttribute.Required == HL7Required.True);

                }

            }
        }

        #endregion

        #region Converting back to ER7 Text
        
        public abstract string GetER7(HL7ParserOptions options);

        #endregion
    }
}
